在 Day 4,我們掌握了 Go 內建的測試工具,學會了 _test.go 的檔案結構和 go test 指令的實用flag,我們現在已經可以完整地執行測試了。
回顧一下我們昨天的測試程式碼:
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}
這段程式碼完全沒有問題,它清晰且有效。但想像一下,如果一個函式需要驗證五、六個不同的結果呢?我們很快就會被一堆 if... t.Errorf(...) 的程式碼所淹沒,這會降低測試的可讀性。
今天的目標: 學習使用 stretchr/testify 套件,將我們的 Assert 從「命令式」升級為「宣告式」,讓測試程式碼更優雅、更易讀。
我們希望測試程式碼讀起來就像在讀一篇規格說明文件。
原本的寫法,讀起來像:
「如果 result 不等於 expected,那麼就標記一個格式為『...』的錯誤。」
我們期望的寫法,讀起來像:
「我在此 assert expected 等同於 result。」
第二種方式更直接,更貼近人類的思考方式,這就是 testify 帶給我們的價值。
testify 是一個第三方套件,它提供了一整套豐富的 assert 工具。由於我們在 Day 2 建立專案時已經使用了 Go Modules,現在安裝它只需要一行指令。
使用 terminal 在專案資料夾中,執行:
go get github.com/stretchr/testify
Go Modules 會自動下載套件,並將其版本資訊記錄在 go.mod 和 go.sum 檔案中。
assert 是 testify 提供的眾多工具之一,它的行為類似於我們之前使用的 t.Errorf —— 當 assert 失敗時,它會將測試標記為失敗,但會繼續執行後續的程式碼。
// calculator_test.go
package main
import (
    "testing"
    "github.com/stretchr/testify/assert" // 導入 assert 套件
)
func TestAddWithAssert(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    // 使用 assert.Equal 進行 assert
    assert.Equal(t, expected, result, "2 + 3 應該等於 5")
}
| 比較項目 | 原生寫法 | testify/assert 寫法 | 
|---|---|---|
| 語法 | if result != expected { ... } | assert.Equal(t, expected, result) | 
| 參數順序 | 容易寫反 result 和 expected 的順序 | 參數順序固定 (t, expected, actual),不易出錯 | 
| 錯誤訊息 | 錯誤訊息需要手動 fmt.Sprintf 風格的格式化 | 錯誤訊息更友好,自動提示「Expected」和「Actual」 | 
| 程式碼簡潔性 | 程式碼更多,意圖被 if 語句包裹 | 程式碼更少,assert.Equal 直接表達「assert 相等」的意圖 | 
assert.Equal 的最後一個參數是可選的,可以用來提供額外的上下文說明訊息,這在除錯時非常有用。
testify 提供了非常豐富的assert函式,幾乎涵蓋了所有你能想到的情境:
| assert 函式 | 功能描述 | 
|---|---|
| assert.NotEqual(t, unexpected, actual) | assert 不相等 | 
| assert.Nil(t, object)/assert.NotNil(t, object) | assert 為 nil / 不為 nil | 
| assert.True(t, value)/assert.False(t, value) | assert 為 true / false | 
| assert.Len(t, object, length) | assert 集合(如 slice, map)的長度 | 
| assert.Contains(t, "Hello World", "Hello") | assert 字串/集合包含某個子項 | 
| assert.NoError(t, err)/assert.Error(t, err) | assert 錯誤為 nil / 不為 nil (這在測試錯誤處理時極其重要!) | 
testify 還提供了另一個幾乎與 assert 完全一樣的套件:require。它們的函式名稱、參數都一模一樣,但有一個關鍵的行為區別:
| 套件 | 失敗後的行為 | 
|---|---|
| assert | assert 失敗後,繼續執行當前的測試函式 | 
| require | assert 失敗後,立即停止執行當前的測試函式(行為類似 t.Fatalf) | 
那麼,我們該用哪一個呢? 這取決於你的測試情境。
經驗法則: 當一個 assert 是後續所有 assert的先決條件時,使用 require,反之,使用 assert。
假設我們在測試一個「建立使用者並回傳資料」的函式。
func TestCreateUser(t *testing.T) {
    // 導入 require 套件
    // import "github.com/stretchr/testify/require"
    // 步驟一:建立使用者,這個過程不應該出錯
    user, err := CreateUser("John Doe")
    
    // 前置條件的檢查:如果建立使用者就失敗了,後續的檢查都沒意義了
    // 所以這裡用 require!
    require.NoError(t, err)
    require.NotNil(t, user)
    // 步驟二:檢查回傳使用者的各個屬性
    // 這裡用 assert,因為檢查 name 和檢查 email 是獨立的
    // 即使 name 不對,我們也想知道 email 是否正確
    assert.Equal(t, "John Doe", user.Name)
    assert.Equal(t, "active", user.Status)
    assert.True(t, user.CreatedAt.Before(time.Now()))
}
在這個例子中,如果 CreateUser 就直接返回了 error 或 nil 的 user,後續的 assert.Equal(t, "John Doe", user.Name) 會立刻引發一個 nil pointer panic,測試會以一種混亂的方式崩潰,使用 require 可以讓測試在第一個前置條件不滿足時就乾淨地停止,並給出清晰的錯誤報告。
今天我們為我們的 TDD 工具箱增加了一件強大的工具: testify。
testify/assert 來撰寫更具可讀性、更宣告式的 assertion。這項技能將極大地提升我們測試程式碼的品質和維護性。
預告:Day 6 - 表格驅動測試 (Table-Driven Tests) - Go 語言的測試慣用法
我們已經學會了如何優雅地測試「一個」情境。但如果我們要測試 Add(2, 3)、Add(-1, 1)、Add(0, 0) 等多個情境呢?是寫好幾個測試函式嗎?明天,我們將使用 Golang中一種模式——「表格驅動測試」,它能讓我們用少量程式碼,優雅地覆蓋多個的測試案例。